<template>
  <div class="device-config">

    <!-- 设备列表管理 -->
    <el-card class="config-card device-list">
      <template #header>
        <div class="card-header">
          <el-icon><Setting /></el-icon>
          <span>设备配置管理</span>
          <div class="header-actions">
            <el-button type="primary" @click="openDeviceDialog()">
              <el-icon><Plus /></el-icon>
              添加设备
            </el-button>
            <el-button @click="loadDevices" :loading="loading">
              <el-icon><Refresh /></el-icon>
              刷新
            </el-button>
            <el-button @click="saveAllConfig" :loading="saving">
              <el-icon><Check /></el-icon>
              保存配置
            </el-button>
          </div>
        </div>
      </template>

      <!-- 搜索和过滤 -->
      <div class="filter-section">
        <el-row :gutter="20" class="mb-3">
          <el-col :xs="24" :sm="8">
            <el-input 
              v-model="searchKeyword" 
              placeholder="搜索设备名称或地址"
              clearable
              @input="handleSearch"
            >
              <template #prefix>
                <el-icon><Search /></el-icon>
              </template>
            </el-input>
          </el-col>
          <el-col :xs="24" :sm="8">
            <el-select 
              v-model="filterProtocol" 
              placeholder="选择协议类型"
              clearable
              @change="handleFilter"
              style="width: 100%"
            >
              <el-option label="所有协议" value="" />
              <el-option label="Modbus TCP" value="tcpmodbus" />
              <el-option label="Modbus RTU" value="rtumodbus" />
              <el-option label="HTTP" value="http" />
              <el-option label="MQTT" value="mqtt" />
            </el-select>
          </el-col>
          <el-col :xs="24" :sm="8">
            <el-select 
              v-model="filterStatus" 
              placeholder="设备状态"
              clearable
              @change="handleFilter"
              style="width: 100%"
            >
              <el-option label="全部状态" value="" />
              <el-option label="在线" value="online" />
              <el-option label="离线" value="offline" />
              <el-option label="故障" value="error" />
            </el-select>
          </el-col>
        </el-row>
      </div>

      <!-- 设备列表 -->
      <el-table 
        :data="filteredDevices" 
        v-loading="loading"
        stripe
        border
        table-layout="fixed"
        style="width: 100%"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="50" fixed="left" />
        
        <el-table-column prop="name" label="设备名称" min-width="160" fixed="left" show-overflow-tooltip>
          <template #default="{ row }">
            <div class="device-name">
              <el-icon :color="getStatusColor(row.status)" size="14">
                <CircleCheckFilled />
              </el-icon>
              <span>{{ row.name }}</span>
            </div>
          </template>
        </el-table-column>
        
        <el-table-column prop="protocol" label="协议类型" width="120">
          <template #default="{ row }">
            <el-tag :type="getProtocolTagType(row.protocol)" size="small">
              {{ getProtocolName(row.protocol) }}
            </el-tag>
          </template>
        </el-table-column>
        
        <!-- 物模型上报频率（动态显示） -->
        <el-table-column label="物模型" min-width="220">
          <template #default="{ row }">
            <div class="model-list" :title="getModelTooltip(row)">
              <div
                v-for="item in getModelDisplayList(row)"
                :key="item.key"
                class="model-list-item"
              >
                <div class="model-list-name">{{ item.label }}</div>
                <div class="model-list-details">{{ item.details }}</div>
              </div>
            </div>
          </template>
        </el-table-column>

        <el-table-column label="地址/端口" width="200" show-overflow-tooltip>
          <template #default="{ row }">
            <span>{{ row.connection?.address || '--' }}</span><span v-if="row.connection?.port">:{{ row.connection.port }}</span>
          </template>
        </el-table-column>

        <el-table-column label="从站ID" width="80">
          <template #default="{ row }">
            {{ row.connection?.slave_id || '--' }}
          </template>
        </el-table-column>
        
        <el-table-column prop="status" label="状态" width="80">
          <template #default="{ row }">
            <span :class="getStatusClass(row.status)">
              {{ getStatusText(row.status) }}
            </span>
          </template>
        </el-table-column>
        
        <el-table-column prop="last_update" label="最后更新" width="150">
          <template #default="{ row }">
            {{ formatTime(row.last_update) }}
          </template>
        </el-table-column>
        
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="{ row }">
            <el-button-group size="small">
              <el-button type="primary" @click="testDevice(row)" :loading="row.testing">
                <el-icon><Link /></el-icon>
                测试
              </el-button>
              <el-button @click="editDevice(row)">
                <el-icon><Edit /></el-icon>
                编辑
              </el-button>
              <el-button type="danger" @click="deleteDevice(row)">
                <el-icon><Delete /></el-icon>
                删除
              </el-button>
            </el-button-group>
          </template>
        </el-table-column>
      </el-table>

      <!-- 批量操作 -->
      <div class="batch-actions" v-if="selectedDevices.length > 0">
        <el-alert 
          :title="`已选择 ${selectedDevices.length} 个设备`"
          type="info"
          show-icon
          :closable="false"
        >
          <template #default>
            <div class="batch-buttons">
              <el-button size="small" @click="batchTest">批量测试</el-button>
              <el-button size="small" @click="batchDelete" type="danger">批量删除</el-button>
            </div>
          </template>
        </el-alert>
      </div>
    </el-card>

    <!-- 设备编辑对话框 -->
    <el-dialog 
      class="device-edit-dialog"
      :title="dialogTitle"
      v-model="deviceDialogVisible" 
      width="900px"
      top="5vh"
      :before-close="handleDialogClose"
    >
      <el-form
        ref="deviceFormRef"
        :model="currentDevice"
        :rules="deviceRules"
        label-width="120px"
        size="default"
      >
        <el-form-item label="通道名称" prop="name" required>
          <el-input
            v-model="currentDevice.name"
            placeholder="系统根据协议自动生成"
            @input="handleChannelNameInput"
          />
          <div class="form-tip">
            <el-icon><InfoFilled /></el-icon>
            <span>可手动调整通道名称，保存时将同步更新相关子设备SN</span>
          </div>
        </el-form-item>

        <el-form-item label="协议类型" prop="protocol" required>
          <el-select 
            v-model="currentDevice.protocol" 
            placeholder="选择协议类型"
            style="width: 100%"
            @change="handleProtocolChange"
          >
            <el-option label="Modbus TCP" value="tcpmodbus" />
            <el-option label="Modbus RTU" value="rtumodbus" />
            <el-option label="HTTP API" value="http" />
            <el-option label="MQTT设备" value="mqtt" />
          </el-select>
        </el-form-item>

        <el-divider content-position="left">连接参数</el-divider>

        <template v-if="isRtuProtocol(currentDevice.protocol)">
          <el-form-item label="串口设备" prop="connection.address" required>
            <el-popover
              placement="bottom-start"
              width="280"
              trigger="manual"
              v-model:visible="serialPortPopoverVisible"
              @show="handleSerialPopoverShow"
              :teleported="false"
            >
              <template #reference>
                <el-input
                  v-model="currentDevice.connection.address"
                  placeholder="/dev/ttyS0 或 COM1"
                  clearable
                >
                  <template #suffix>
                    <el-tooltip content="点击查找串口" placement="top">
                      <el-icon
                        class="serial-port-search"
                        @click.stop="handleSerialPortSearch"
                      >
                        <Search />
                      </el-icon>
                    </el-tooltip>
                  </template>
                </el-input>
              </template>
              <div class="serial-port-popover">
                <div class="serial-port-popover__header">
                  <span>检测到串口</span>
                  <el-button
                    type="primary"
                    link
                    size="small"
                    @click="refreshSerialPorts"
                    :loading="serialPortLoading"
                  >刷新</el-button>
                </div>
                <el-scrollbar height="180">
                  <el-skeleton v-if="serialPortLoading" :rows="3" animated />
                  <el-empty
                    v-else-if="!serialPortOptions.length"
                    description="未检测到串口"
                  />
                  <el-radio-group
                    v-else
                    v-model="currentDevice.connection.address"
                    class="serial-port-list"
                    @change="handleSerialPortSelected"
                  >
                    <el-radio
                      v-for="port in serialPortOptions"
                      :key="port"
                      :label="port"
                    >
                      {{ port }}
                    </el-radio>
                  </el-radio-group>
                </el-scrollbar>
              </div>
            </el-popover>
          </el-form-item>

          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="串口类型" prop="connection.serial_type" required>
                <el-select v-model="currentDevice.connection.serial_type" style="width: 100%">
                  <el-option
                    v-for="item in serialTypeOptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="波特率" prop="connection.baud_rate" required>
                <el-select v-model="currentDevice.connection.baud_rate" style="width: 100%">
                  <el-option
                    v-for="rate in baudRateOptions"
                    :key="rate"
                    :label="rate"
                    :value="rate"
                  />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>

          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="数据位" prop="connection.data_bits" required>
                <el-select v-model="currentDevice.connection.data_bits" style="width: 100%">
                  <el-option
                    v-for="bit in dataBitOptions"
                    :key="bit"
                    :label="bit"
                    :value="bit"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="停止位" prop="connection.stop_bits" required>
                <el-select v-model="currentDevice.connection.stop_bits" style="width: 100%">
                  <el-option
                    v-for="bit in stopBitOptions"
                    :key="bit"
                    :label="bit"
                    :value="bit"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="校验位" prop="connection.parity" required>
                <el-select v-model="currentDevice.connection.parity" style="width: 100%">
                  <el-option
                    v-for="item in parityOptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>

          <el-form-item label="连接延时(ms)" prop="connection.connection_delay">
            <el-input-number
              v-model="currentDevice.connection.connection_delay"
              :min="0"
              :max="60000"
              :step="10"
              controls-position="right"
              class="number-input"
              placeholder="串口初始化延时"
            />
          </el-form-item>
        </template>
        <template v-else>
          <el-row :gutter="20">
            <el-col :span="14">
              <el-form-item label="设备地址" prop="connection.address" required>
                <el-input
                  v-model="currentDevice.connection.address"
                  placeholder="IP地址或设备地址"
                  clearable
                />
              </el-form-item>
            </el-col>
            <el-col :span="10">
              <el-form-item label="端口" prop="connection.port" required>
                <el-input-number
                  v-model="currentDevice.connection.port"
                  :min="1"
                  :max="65535"
                  controls-position="right"
                  class="number-input"
                  placeholder="端口号"
                />
              </el-form-item>
            </el-col>
          </el-row>
        </template>

        <el-form-item
          label="从站ID"
          prop="connection.slave_id"
          v-if="isModbusProtocol(currentDevice.protocol)"
        >
          <el-input-number
            v-model="currentDevice.connection.slave_id"
            :min="1"
            :max="255"
            controls-position="right"
            class="number-input"
            placeholder="Modbus从站ID"
          />
        </el-form-item>

        <el-form-item label="连接超时(ms)" prop="connection.timeout">
          <el-input-number
            v-model="currentDevice.connection.timeout"
            :min="1000"
            :max="30000"
            :step="1000"
            controls-position="right"
            class="number-input"
            placeholder="连接超时时间"
          />
        </el-form-item>

        <el-row :gutter="20" v-if="isRtuProtocol(currentDevice.protocol)">
          <el-col :span="12">
            <el-form-item label="超时重连">
              <el-checkbox v-model="currentDevice.connection.reconnect_on_timeout">
                Reconnect on timeout
              </el-checkbox>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="重连超时(ms)" prop="connection.reconnect_timeout">
              <el-input-number
                v-model="currentDevice.connection.reconnect_timeout"
                :min="100"
                :max="60000"
                :step="100"
                controls-position="right"
                class="number-input"
                placeholder="重连等待时间"
                :disabled="!currentDevice.connection.reconnect_on_timeout"
              />
            </el-form-item>
          </el-col>
        </el-row>

        <el-form-item label="轮询间隔(秒)" prop="poll_interval">
          <el-input-number
            v-model="currentDevice.poll_interval"
            :min="1"
            :max="86400"
            :step="1"
            controls-position="right"
            class="number-input"
            placeholder="数据轮询间隔（秒）"
          />
        </el-form-item>

        <el-divider content-position="left">物模型上报</el-divider>
        <div v-if="modelLoading" class="model-loading">
          <el-skeleton :rows="2" animated />
        </div>
        <template v-else>
          <el-alert
            v-if="availableModelKeys.length === 0"
            type="info"
            :closable="false"
            class="model-empty-tip"
            title="当前协议未定义物模型分类"
          >
            <template #default>
              <span>请检查协议文件中数据点的 device_category 字段。</span>
            </template>
          </el-alert>
          <div v-else class="model-blocks">
            <div
              v-for="category in availableModelKeys"
              :key="category"
              class="model-block"
            >
              <div class="model-block-header">
                <span class="model-block-title">{{ getModelLabel(category) }}</span>
                <el-switch
                  v-model="currentDevice.models[category].enabled"
                  size="small"
                />
              </div>
              <div class="model-block-body">
                <el-form-item
                  :label="`${getModelLabel(category)}上报(秒)`"
                  label-width="auto"
                  class="model-form-item"
                >
                  <el-input-number
                    v-model="currentDevice.models[category].upload_interval"
                    :min="1"
                    :max="86400"
                    :step="1"
                    controls-position="right"
                    class="number-input"
                  />
                </el-form-item>
                <el-form-item
                  :label="`${getModelLabel(category)}类型ID`"
                  label-width="auto"
                  class="model-form-item"
                >
                  <el-input
                    v-model="currentDevice.models[category].deviceType"
                    readonly
                    class="model-readonly"
                  />
                </el-form-item>
                <el-form-item
                  :label="`${getModelLabel(category)}设备序列号`"
                  label-width="auto"
                  class="model-form-item"
                >
                  <el-input
                    v-model="currentDevice.models[category].deviceSn"
                    readonly
                    class="model-readonly"
                  />
                </el-form-item>
                <div class="model-tip">
                  <el-icon><InfoFilled /></el-icon>
                  <span>设备名称：{{ currentDevice.models[category].deviceName }}</span>
                </div>
              </div>
            </div>
          </div>
      </template>

        <el-form-item label="协议配置" prop="protocol_config" required>
          <el-select
            v-model="currentDevice.protocol_config"
            placeholder="选择协议配置文件"
            style="width: 100%"
            :loading="protocolsLoading"
          >
            <el-option
              v-for="protocol in protocolOptions"
              :key="protocol.value"
              :label="protocol.label"
              :value="protocol.value"
            />
          </el-select>
          <div class="form-tip">
            <el-icon><InfoFilled /></el-icon>
            <span>协议配置文件定义了设备的数据点映射和通信参数</span>
          </div>
        </el-form-item>

        <el-form-item label="设备描述" prop="description">
          <el-input
            v-model="currentDevice.description"
            type="textarea"
            :rows="3"
            placeholder="设备描述信息（可选）"
            maxlength="255"
            show-word-limit
          />
        </el-form-item>
      </el-form>

      <template #footer>
        <div class="dialog-footer">
          <el-button @click="deviceDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveDevice" :loading="saving">
            {{ isEditing ? '更新' : '添加' }}
          </el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
import {
  Setting, Plus, Refresh, Check, Search, Link, Edit, Delete,
  CircleCheckFilled, InfoFilled
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { configAPI, deviceAPI, protocolAPI, modbusAPI } from '@/utils/api'


// 响应式数据
const loading = ref(false)
const saving = ref(false)
const devices = ref([])
const selectedDevices = ref([])
const searchKeyword = ref('')
const filterProtocol = ref('')
const filterStatus = ref('')

// 协议相关
const protocolsLoading = ref(false)
const protocolOptions = ref([])
const protocolModelCache = reactive({})
const availableModelKeys = ref([])
const modelLoading = ref(false)

// 网关标识（用于组合物模型deviceSn）
const gatewayId = ref('')

// 对话框相关
const deviceDialogVisible = ref(false)
const isEditing = ref(false)
const deviceFormRef = ref()
const channelNameCustomized = ref(false)
const serialPortPopoverVisible = ref(false)
const serialPortOptions = ref([])
const serialPortLoading = ref(false)
const serialPortFetched = ref(false)

const createDefaultConnection = () => ({
  address: '',
  port: 502,
  slave_id: 1,
  timeout: 5000,
  serial_type: 'rtu-buffered',
  baud_rate: 9600,
  data_bits: 8,
  stop_bits: 1,
  parity: 'N',
  connection_delay: 100,
  reconnect_on_timeout: true,
  reconnect_timeout: 2000
})

const serialTypeOptions = [
  { label: 'RTU', value: 'rtu' },
  { label: 'RTU-BUFFERED', value: 'rtu-buffered' }
]

const baudRateOptions = [1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
const dataBitOptions = [5, 6, 7, 8]
const stopBitOptions = [1, 2]
const parityOptions = [
  { label: 'None', value: 'N' },
  { label: 'Even', value: 'E' },
  { label: 'Odd', value: 'O' }
]

const isRtuProtocol = (protocol) => {
  const value = (protocol || '').toLowerCase()
  return value === 'rtumodbus' || value === 'modbus_rtu'
}

const toNumber = (value, fallback) => {
  if (value === '' || value === null || value === undefined) return fallback
  const num = Number(value)
  return Number.isFinite(num) ? num : fallback
}

const normalizeConnectionConfig = (raw, legacy = {}, options = {}) => {
  const base = createDefaultConnection()
  const mergedSource = { ...legacy, ...(raw || {}) }
  const normalized = { ...base, ...mergedSource }

  const addressSource = mergedSource.address ?? mergedSource.serial_port ?? legacy.address ?? ''
  normalized.address = addressSource == null ? '' : String(addressSource).trim()

  normalized.port = toNumber(normalized.port, base.port)
  normalized.slave_id = toNumber(normalized.slave_id, base.slave_id)
  normalized.timeout = toNumber(normalized.timeout, base.timeout)
  normalized.baud_rate = toNumber(normalized.baud_rate, base.baud_rate)
  normalized.data_bits = toNumber(normalized.data_bits, base.data_bits)
  normalized.stop_bits = toNumber(normalized.stop_bits, base.stop_bits)
  normalized.connection_delay = toNumber(normalized.connection_delay, base.connection_delay)
  normalized.reconnect_timeout = toNumber(normalized.reconnect_timeout, base.reconnect_timeout)

  const reconnectRaw = mergedSource.reconnect_on_timeout
  if (reconnectRaw === undefined || reconnectRaw === null || reconnectRaw === '') {
    normalized.reconnect_on_timeout = base.reconnect_on_timeout
  } else if (typeof reconnectRaw === 'string') {
    normalized.reconnect_on_timeout = reconnectRaw.toLowerCase() !== 'false' && reconnectRaw !== '0'
  } else {
    normalized.reconnect_on_timeout = Boolean(reconnectRaw)
  }

  const inferredIsRtu = typeof options.isRtu === 'boolean'
    ? options.isRtu
    : (typeof options.protocol === 'string' ? isRtuProtocol(options.protocol) : undefined)

  if (inferredIsRtu) {
    delete normalized.port
  }

  return normalized
}

const fetchSerialPorts = async (force = false) => {
  if (!isRtuProtocol(currentDevice.protocol)) {
    serialPortOptions.value = []
    return
  }

  if (!force && serialPortFetched.value && serialPortOptions.value.length) {
    return
  }

  serialPortLoading.value = true
  try {
    const response = await modbusAPI.listSerialPorts()
    const ports = Array.isArray(response?.ports) ? response.ports : []
    serialPortOptions.value = ports
    serialPortFetched.value = true
  } catch (error) {
    console.error('串口列表获取失败:', error)
    ElMessage.error('串口列表获取失败，请手动填写')
  } finally {
    serialPortLoading.value = false
  }
}

const handleSerialPortSearch = async () => {
  if (!isRtuProtocol(currentDevice.protocol)) {
    ElMessage.warning('请选择 Modbus RTU 协议后再查找串口')
    return
  }

  await fetchSerialPorts(!serialPortFetched.value)
  serialPortPopoverVisible.value = true
}

const refreshSerialPorts = async () => {
  await fetchSerialPorts(true)
}

const handleSerialPortSelected = () => {
  serialPortPopoverVisible.value = false
}

const handleSerialPopoverShow = async () => {
  if (!serialPortOptions.value.length) {
    await fetchSerialPorts(true)
  }
}

// 当前编辑的设备
const currentDevice = reactive({
  id: null,
  name: '',
  protocol: '',
  connection: createDefaultConnection(),
  poll_interval: 1000,
  upload_interval: 10,
  enabled: true,
  description: '',
  status: 'offline',
  last_update: null,
  testing: false,
  protocol_config: '',
  type: '',
  models: {}
})

// 设备默认值（兼容新旧协议命名）
const deviceDefaults = {
  tcpmodbus: { port: 502 },
  rtumodbus: {},
  modbus_tcp: { port: 502 },
  modbus_rtu: {},
  http: { port: 80 },
  mqtt: { port: 1883 }
}

// 表单验证规则
const deviceRules = {
  name: [
    { required: true, message: '请输入设备名称', trigger: 'blur' },
    { min: 2, max: 50, message: '设备名称长度在 2 到 50 个字符', trigger: 'blur' },
    {
      validator: (rule, value, callback) => {
        if (!value) {
          callback()
          return
        }
        if (value.includes('_')) {
          callback(new Error('设备名称不能包含下划线(_)字符'))
          return
        }
        const conflict = devices.value.some(device => {
          if (!device) return false
          if (device.id === currentDevice.id) return false
          return (device.name || '').toLowerCase() === value.toLowerCase()
        })
        if (conflict) {
          callback(new Error('通道名称已存在，请输入唯一名称'))
          return
        }
        callback()
      },
      trigger: 'blur'
    }
  ],
  protocol: [
    { required: true, message: '请选择协议类型', trigger: 'change' }
  ],
  'connection.address': [
    {
      validator: (rule, value, callback) => {
        if (isRtuProtocol(currentDevice.protocol)) {
          if (!value || !String(value).trim()) {
            callback(new Error('请输入串口设备路径'))
            return
          }
        } else {
          if (!value || !String(value).trim()) {
            callback(new Error('请输入设备地址'))
            return
          }
        }
        callback()
      },
      trigger: 'blur'
    }
  ],
  'connection.port': [
    {
      validator: (rule, value, callback) => {
        if (isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        if (value === null || value === undefined || value === '') {
          callback(new Error('请输入端口号'))
          return
        }
        const num = Number(value)
        if (!Number.isFinite(num) || num < 1 || num > 65535) {
          callback(new Error('端口号范围1-65535'))
          return
        }
        callback()
      },
      trigger: 'blur'
    }
  ],
  'connection.serial_type': [
    {
      validator: (rule, value, callback) => {
        if (!isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        if (!value) {
          callback(new Error('请选择串口类型'))
          return
        }
        callback()
      },
      trigger: 'change'
    }
  ],
  'connection.baud_rate': [
    {
      validator: (rule, value, callback) => {
        if (!isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        if (!value) {
          callback(new Error('请选择波特率'))
          return
        }
        callback()
      },
      trigger: 'change'
    }
  ],
  'connection.data_bits': [
    {
      validator: (rule, value, callback) => {
        if (!isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        if (!Number.isInteger(Number(value))) {
          callback(new Error('请选择数据位'))
          return
        }
        callback()
      },
      trigger: 'change'
    }
  ],
  'connection.stop_bits': [
    {
      validator: (rule, value, callback) => {
        if (!isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        const num = Number(value)
        if (!Number.isFinite(num)) {
          callback(new Error('请选择停止位'))
          return
        }
        callback()
      },
      trigger: 'change'
    }
  ],
  'connection.parity': [
    {
      validator: (rule, value, callback) => {
        if (!isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        if (!value) {
          callback(new Error('请选择校验位'))
          return
        }
        callback()
      },
      trigger: 'change'
    }
  ],
  'connection.connection_delay': [
    {
      validator: (rule, value, callback) => {
        if (!isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        const num = Number(value)
        if (!Number.isFinite(num) || num < 0) {
          callback(new Error('请输入连接延时（>=0）'))
          return
        }
        callback()
      },
      trigger: 'blur'
    }
  ],
  'connection.reconnect_timeout': [
    {
      validator: (rule, value, callback) => {
        if (!isRtuProtocol(currentDevice.protocol)) {
          callback()
          return
        }
        if (!currentDevice.connection.reconnect_on_timeout) {
          callback()
          return
        }
        const num = Number(value)
        if (!Number.isFinite(num) || num <= 0) {
          callback(new Error('请输入重连超时时间'))
          return
        }
        callback()
      },
      trigger: 'blur'
    }
  ],
  protocol_config: [
    { required: true, message: '请选择协议配置文件', trigger: 'change' }
  ]
}

// 计算属性
const filteredDevices = computed(() => {
  let filtered = devices.value

  // 关键词搜索
  if (searchKeyword.value) {
    const keyword = searchKeyword.value.toLowerCase()
    filtered = filtered.filter(device =>
      device.name.toLowerCase().includes(keyword) ||
      (device.connection?.address || '').toLowerCase().includes(keyword)
    )
  }

  // 协议过滤
  if (filterProtocol.value) {
    filtered = filtered.filter(device => device.protocol === filterProtocol.value)
  }

  // 状态过滤
  if (filterStatus.value) {
    filtered = filtered.filter(device => device.status === filterStatus.value)
  }

  return filtered
})

const dialogTitle = computed(() => {
  return isEditing.value ? '编辑设备' : '添加设备'
})

const MODEL_LABEL_MAP = {
  bms: 'BMS',
  pcs: 'PCS',
  alarm: 'ALARM'
}

const MODEL_TYPE_MAP = {
  bms: 10133,
  pcs: 10134,
  alarm: 10132
}

const DEFAULT_MODEL_INTERVAL = 10

const sanitizePrefix = (value) => {
  if (!value) return 'channel'
  return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'channel'
}

const getGatewayIdentifier = (gwId) => {
  if (gwId === undefined || gwId === null) return ''
  const trimmed = String(gwId).trim().toLowerCase()
  if (!trimmed) return ''
  const withoutPrefix = trimmed.replace(/^ems[-_]/, '')
  const normalized = sanitizePrefix(withoutPrefix)
  if (normalized && normalized !== 'channel') return normalized
  const fallback = sanitizePrefix(trimmed)
  if (fallback.startsWith('ems-') && fallback.length > 4) {
    return fallback.slice(4)
  }
  return fallback !== 'channel' ? fallback : ''
}

const extractIpLastOctet = (address) => {
  if (address === undefined || address === null) return ''
  let trimmed = String(address).trim()
  if (!trimmed) return ''
  const colonPos = trimmed.indexOf(':')
  if (colonPos >= 0) {
    trimmed = trimmed.slice(0, colonPos)
  }
  if (!trimmed) return ''
  const parts = trimmed.split('.')
  if (parts.length < 2) return ''
  if (!parts.every(part => /^\d+$/.test(part))) return ''
  return parts[parts.length - 1]
}

const parsePositiveInt = (value) => {
  const num = Number(value)
  if (Number.isFinite(num) && num > 0) return Math.floor(num)
  return null
}

const extractIndexFromSn = (sn) => {
  if (!sn) return 0
  const match = String(sn).trim().match(/-(\d+)$/)
  return match ? Number(match[1]) : 0
}

const extractIndexFromKey = (key) => {
  if (!key) return 0
  const match = String(key).trim().match(/-(\d+)$/)
  return match ? Number(match[1]) : 0
}

const normalizeModelKeyBase = (value) => {
  if (value === undefined || value === null) return 'model'
  const normalized = String(value).trim().toLowerCase()
  if (!normalized) return 'model'
  const sanitized = normalized.replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '')
  if (!sanitized) return 'model'
  const base = sanitized.replace(/-(\d+)$/, '')
  return base || sanitized
}

const composeModelKey = (base, index) => {
  const normalizedBase = normalizeModelKeyBase(base)
  if (!Number.isFinite(index) || index <= 0) {
    return normalizedBase || 'model'
  }
  return `${normalizedBase || 'model'}-${index}`
}

const getMaxModelIndexForBase = (base, excludeId = null) => {
  const normalizedBase = normalizeModelKeyBase(base)
  let maxIndex = 0
  const consider = (idx) => {
    if (Number.isFinite(idx) && idx > maxIndex) {
      maxIndex = idx
    }
  }
  devices.value.forEach(device => {
    if (!device || (excludeId && device.id === excludeId)) return
    const models = device.models || {}
    for (const [key, model] of Object.entries(models)) {
      if (normalizeModelKeyBase(key) !== normalizedBase) continue
      if (!model) {
        consider(1)
        continue
      }
      let idx = Number(model.index)
      if (!Number.isFinite(idx) || idx <= 0) idx = extractIndexFromKey(key)
      if (!Number.isFinite(idx) || idx <= 0) idx = extractIndexFromSn(model.deviceSn)
      if (!Number.isFinite(idx) || idx <= 0) idx = extractIndexFromKey(model.deviceName)
      if (!Number.isFinite(idx) || idx <= 0) idx = 1
      consider(idx)
    }
  })
  return maxIndex
}

const computeDefaultModelIndex = (category, excludeId = null, taken = null) => {
  const base = normalizeModelKeyBase(category)
  let candidate = getMaxModelIndexForBase(base, excludeId) + 1
  if (!Number.isFinite(candidate) || candidate <= 0) {
    candidate = 1
  }
  if (taken && taken.size) {
    const set = taken.get(base)
    if (set) {
      while (set.has(candidate)) {
        candidate += 1
      }
    }
  }
  return candidate
}

const normalizeModels = (input) => {
  const source = input && typeof input === 'object' ? input : {}
  const result = {}
  Object.entries(source).forEach(([key, value]) => {
    if (!key) return
    const item = value || {}
    const interval = Number(item.upload_interval ?? item.uploadInterval)
    const deviceType = Number(item.deviceType ?? item.device_type)
    const deviceSn = (item.deviceSn || item.device_sn || '').trim()
    const deviceName = (item.deviceName || item.name || '').trim()
    const index = Number(item.index ?? item.modelIndex ?? 0)
    result[key] = {
      enabled: item.enabled !== false,
      upload_interval: Number.isFinite(interval) && interval > 0 ? Math.round(interval) : DEFAULT_MODEL_INTERVAL,
      deviceType: Number.isFinite(deviceType) && deviceType > 0 ? deviceType : 0,
      deviceSn,
      deviceName,
      index: Number.isFinite(index) && index > 0 ? Math.round(index) : 0
    }
  })
  return result
}

const mergeModelsWithCategories = (existing, categories, options = {}) => {
  const normalized = normalizeModels(existing)
  const remaining = new Map(Object.entries(normalized))
  const result = {}
  const usedIndices = new Map()
  categories.forEach((category) => {
    if (!category) return
    const baseKey = normalizeModelKeyBase(category)
    let prev = {}
    if (remaining.has(category)) {
      prev = remaining.get(category) || {}
      remaining.delete(category)
    } else {
      for (const [key, value] of remaining.entries()) {
        if (normalizeModelKeyBase(key) === baseKey) {
          prev = value || {}
          remaining.delete(key)
          break
        }
      }
    }
    const deviceIdSource = options.deviceId !== undefined ? options.deviceId : currentDevice.id
    const channelName = options.channelName || currentDevice.name || ''
    const gatewayIdValue = options.gatewayId || gatewayId.value || ''
    const gatewayIdentifier = getGatewayIdentifier(gatewayIdValue)

    const usedForBase = usedIndices.get(baseKey) || new Set()
    let index = Number(prev.index)
    if (!Number.isFinite(index) || index <= 0) {
      const fallbackByKey = extractIndexFromKey(category)
      const fallbackPrevKey = extractIndexFromKey(prev.deviceName || '')
      const fallbackSn = extractIndexFromSn(prev.deviceSn)
      index = fallbackByKey || fallbackPrevKey || fallbackSn
    }
    if (!Number.isFinite(index) || index <= 0) {
      index = computeDefaultModelIndex(category, deviceIdSource || null, usedIndices)
    }
    while (usedForBase.has(index)) {
      index += 1
    }
    usedForBase.add(index)
    usedIndices.set(baseKey, usedForBase)

    let deviceSn = (prev.deviceSn || '').trim()
    const categorySegment = category && String(category).trim()
      ? String(category).trim()
      : composeModelKey(baseKey || 'model', index)
    const defaultSnParts = []
    if (channelName) defaultSnParts.push(channelName)
    if (gatewayIdentifier) defaultSnParts.push(gatewayIdentifier)
    if (categorySegment) defaultSnParts.push(categorySegment)
    const defaultSn = defaultSnParts.filter(Boolean).join('-')

    if (!deviceSn) {
      deviceSn = defaultSn
    }

    if (gatewayIdentifier && deviceSn && !deviceSn.toLowerCase().includes(gatewayIdentifier.toLowerCase())) {
      const parts = deviceSn.split('-').filter(part => part)
      if (parts.length === 0) {
        deviceSn = defaultSn
      } else {
        const channelLower = (channelName || '').toLowerCase()
        const insertIndex = channelLower && parts[0] && parts[0].toLowerCase() === channelLower ? 1 : 0
        parts.splice(insertIndex, 0, gatewayIdentifier)
        deviceSn = parts.join('-')
      }
    }

    const expectedSuffix = categorySegment || composeModelKey(baseKey, index)
    if (!deviceSn) {
      const fallbackParts = [channelName || 'channel']
      if (gatewayIdentifier) fallbackParts.push(gatewayIdentifier)
      fallbackParts.push(expectedSuffix)
      deviceSn = fallbackParts.filter(Boolean).join('-')
    } else {
      const suffixBase = normalizeModelKeyBase(expectedSuffix)
      const snParts = deviceSn.split('-').filter(part => part)
      while (snParts.length > 0) {
        const last = snParts[snParts.length - 1]
        const lastNorm = normalizeModelKeyBase(last)
        if (/^\d+$/.test(last)) {
          snParts.pop()
          continue
        }
        if (lastNorm === suffixBase && last !== expectedSuffix) {
          snParts.pop()
          continue
        }
        break
      }
      snParts.push(expectedSuffix)
      deviceSn = snParts.join('-')
    }

    const deviceType = prev.deviceType && prev.deviceType > 0
      ? prev.deviceType
      : (MODEL_TYPE_MAP[baseKey] || 0)
    const expectedName = category
    const prevName = (prev.deviceName || '').trim()
    const deviceName = (!prevName || normalizeModelKeyBase(prevName) === baseKey)
      ? expectedName
      : prevName
    result[category] = {
      enabled: prev.enabled !== false,
      upload_interval: prev.upload_interval && prev.upload_interval > 0 ? prev.upload_interval : DEFAULT_MODEL_INTERVAL,
      deviceType,
      deviceSn,
      deviceName,
      index
    }
  })
  return result
}

const generateUniqueModelCategories = (categories) => {
  const list = Array.isArray(categories) ? categories.filter(Boolean) : []
  const seen = new Set()
  const result = []
  list.forEach((category) => {
    const value = String(category ?? '').trim()
    if (!value) return
    const lower = value.toLowerCase()
    if (seen.has(lower)) return
    seen.add(lower)
    result.push(value)
  })
  return result
}

const prepareModelCategories = (protocolCategories, device = currentDevice) => {
  const rawCategories = generateUniqueModelCategories(protocolCategories)
  if (!device) {
    return rawCategories
  }

  const models = device.models && typeof device.models === 'object' ? device.models : {}
  const existingKeys = Object.keys(models)
  const matchedExisting = new Set()
  const finalKeys = rawCategories.map((category) => {
    if (Object.prototype.hasOwnProperty.call(models, category)) {
      matchedExisting.add(category)
      return category
    }
    const base = normalizeModelKeyBase(category)
    const alt = existingKeys.find(key => !matchedExisting.has(key) && normalizeModelKeyBase(key) === base)
    if (alt) {
      matchedExisting.add(alt)
    }
    return category
  })

  existingKeys.forEach((key) => {
    if (matchedExisting.has(key)) return
    const base = normalizeModelKeyBase(key)
    if (finalKeys.some(cat => normalizeModelKeyBase(cat) === base)) return
    finalKeys.push(key)
  })

  const seen = new Set()
  return finalKeys.filter((key) => {
    if (!key) return false
    const lower = key.toLowerCase()
    if (seen.has(lower)) return false
    seen.add(lower)
    return true
  })
}

const getModelLabel = (category) => {
  if (!category) return '未命名'
  const base = category.split('-')[0]
  return MODEL_LABEL_MAP[base] || category
}

const computeSequentialIndex = (prefix, excludeId = null) => {
  let max = 0
  const normalizedPrefix = prefix.toLowerCase()
  devices.value.forEach(device => {
    if (!device || (excludeId && device.id === excludeId)) return
    const name = (device.name || '').toLowerCase()
    if (!name.startsWith(`${normalizedPrefix}-`)) return
    const match = name.match(/-(\d+)(?:-\d+)?$/)
    if (!match) return
    const idx = Number(match[1])
    if (Number.isFinite(idx) && idx > max) {
      max = idx
    }
  })
  return max + 1
}

const ensureUniqueChannelName = (baseName, excludeId = null, taken = null) => {
  if (!baseName) return baseName
  let candidate = baseName
  let counter = 2
  const isConflict = (name) => {
    const lower = name.toLowerCase()
    if (taken && taken.has(lower)) return true
    return devices.value.some(device => device && device.id !== excludeId && (device.name || '').toLowerCase() === lower)
  }
  while (isConflict(candidate)) {
    candidate = `${baseName}-${counter++}`
  }
  if (taken) {
    taken.add(candidate.toLowerCase())
  }
  return candidate
}

const computeChannelName = (device, taken = null) => {
  const proto = (device.protocol || '').toLowerCase()
  const conn = device.connection || {}
  const excludeId = device.id ?? null
  if (!proto) {
    const prefix = 'channel'
    const idx = computeSequentialIndex(prefix, excludeId)
    return ensureUniqueChannelName(`${prefix}-${idx}`, excludeId, taken)
  }

  if (proto.includes('rtu')) {
    const slave = parsePositiveInt(conn.slave_id)
    const idx = slave ?? computeSequentialIndex('rtu', excludeId)
    return ensureUniqueChannelName(`rtu-${idx}`, excludeId, taken)
  }

  if (proto.includes('tcp')) {
    const rawAddressSource = conn.address ?? conn.ip ?? conn.host
    const rawAddress = rawAddressSource == null ? '' : String(rawAddressSource).trim()
    if (rawAddress) {
      const lastOctet = extractIpLastOctet(rawAddress)
      if (lastOctet) {
        return ensureUniqueChannelName(`tcp-${lastOctet}`, excludeId, taken)
      }
      const sanitizedAddress = sanitizePrefix(rawAddress)
      if (sanitizedAddress && sanitizedAddress !== 'channel') {
        return ensureUniqueChannelName(`tcp-${sanitizedAddress}`, excludeId, taken)
      }
    }
    const port = conn.port
    if (port !== undefined && port !== null && String(port).trim() !== '') {
      const portStr = String(port).trim()
      const sanitizedPort = sanitizePrefix(portStr)
      const suffix = sanitizedPort && sanitizedPort !== 'channel' ? sanitizedPort : portStr
      return ensureUniqueChannelName(`tcp-${suffix}`, excludeId, taken)
    }
    const idx = computeSequentialIndex('tcp', excludeId)
    return ensureUniqueChannelName(`tcp-${idx}`, excludeId, taken)
  }

  const prefix = sanitizePrefix(proto)
  const candidate = parsePositiveInt(conn.port) || parsePositiveInt(conn.slave_id)
  const idx = candidate ?? computeSequentialIndex(prefix, excludeId)
  return ensureUniqueChannelName(`${prefix}-${idx}`, excludeId, taken)
}

const getModelDeviceSn = (category) => {
  if (!category) return '--'
  const model = currentDevice.models?.[category]
  if (model?.deviceSn) return model.deviceSn
  const base = gatewayId.value
  if (!base) return '未配置'
  const channelName = currentDevice.name || 'channel'
  const identifier = getGatewayIdentifier(base)
  if (!identifier) return `${channelName}-${category}`
  return `${channelName}-${identifier}-${category}`
}

const handleChannelNameInput = (value) => {
  if (!deviceDialogVisible.value) return
  channelNameCustomized.value = true
  if (typeof value === 'string') {
    const trimmed = value.trim()
    if (trimmed !== value) {
      currentDevice.name = trimmed
    }
  }
}


// 方法
const loadDevices = async () => {
  try {
    loading.value = true
    const config = await configAPI.getDeviceConfig()
    const takenNames = new Set()
    // 将后端毫秒转换为前端秒
    const list = (config.devices || []).map(d => {
      const c = { ...d }
      // 兼容旧字段协议命名 -> 规范为新命名
      if (c.protocol === 'modbus_tcp') c.protocol = 'tcpmodbus'
      if (c.protocol === 'modbus_rtu') c.protocol = 'rtumodbus'
      const legacyConnection = {
        address: c.address,
        port: c.port,
        slave_id: c.slave_id,
        timeout: c.timeout
      }
      c.connection = normalizeConnectionConfig(c.connection, legacyConnection, {
        protocol: c.protocol,
        isRtu: isRtuProtocol(c.protocol)
      })
      delete c.address
      delete c.port
      delete c.slave_id
      delete c.timeout
      if (typeof c.poll_interval === 'number' && c.poll_interval > 0) {
        c.poll_interval = Math.max(1, Math.round(c.poll_interval / 1000))
      }
      c.name = c.name || computeChannelName(c, takenNames)
      takenNames.add((c.name || '').toLowerCase())
      c.models = mergeModelsWithCategories(c.models, Object.keys(c.models || {}), {
        deviceId: c.id,
        channelName: c.name,
        gatewayId: gatewayId.value
      })
      const { deviceType, deviceSn, onlineState, subDeviceCount, ...cleaned } = c
      return cleaned
    })
    devices.value = list
  } catch (error) {
    ElMessage.error('加载设备配置失败: ' + error.message)
  } finally {
    loading.value = false
  }
}

// 加载协议列表
const loadProtocols = async () => {
  try {
    protocolsLoading.value = true
    const response = await protocolAPI.getProtocolList()
    protocolOptions.value = response.protocols || []
  } catch (error) {
    ElMessage.error('加载协议列表失败: ' + error.message)
    console.error('协议列表加载错误:', error)
  } finally {
    protocolsLoading.value = false
  }
}

const loadGatewayIdentity = async () => {
  try {
    const mqttConfig = await configAPI.getMqttConfig()
    const deviceSn = mqttConfig?.client?.device_sn?.trim()
    const clientId = mqttConfig?.client?.client_id?.trim()
    const raw = deviceSn || clientId || ''
    if (!raw) {
      gatewayId.value = ''
    } else if (raw.startsWith('ems-')) {
      gatewayId.value = raw
    } else {
      gatewayId.value = `ems-${raw}`
    }
  } catch (error) {
    console.error('加载网关标识失败:', error)
    gatewayId.value = ''
  }
}

const fetchProtocolCategories = async (protocolPath) => {
  if (!protocolPath) return []
  if (protocolModelCache[protocolPath]) return protocolModelCache[protocolPath]

  const option = (protocolOptions.value || []).find(item => item.value === protocolPath)
  if (option && Array.isArray(option.categories)) {
    const categories = Array.from(new Set(option.categories.filter(Boolean)))
    protocolModelCache[protocolPath] = categories
    return categories
  }

  // 兜底：尝试直接读取协议文件
  const targetPath = protocolPath.startsWith('/') ? protocolPath : `/${protocolPath}`
  const response = await fetch(targetPath, { cache: 'no-store' })
  if (!response.ok) {
    throw new Error(`协议文件加载失败 (HTTP ${response.status})`)
  }
  const data = await response.json()
  const categories = Array.from(new Set((data.data_points || [])
    .map(item => item.device_category)
    .filter(Boolean)))
  protocolModelCache[protocolPath] = categories
  return categories
}

const syncDeviceModelsWithProtocol = async (protocolPath, device = currentDevice) => {
  availableModelKeys.value = []
  if (!protocolPath) {
    device.models = {}
    return
  }

  modelLoading.value = true
  try {
    const rawCategories = await fetchProtocolCategories(protocolPath)
    const categories = prepareModelCategories(rawCategories, device)
    device.models = mergeModelsWithCategories(device.models, categories, {
      deviceId: device.id ?? currentDevice.id ?? null,
      channelName: device.name || currentDevice.name || computeChannelName(device),
      gatewayId: gatewayId.value
    })
    availableModelKeys.value = categories
  } catch (error) {
    console.error('同步协议物模型失败:', error)
    ElMessage.error('读取协议的物模型定义失败，请检查协议文件')
    const fallbackCategories = Object.keys(device.models || {})
    device.models = mergeModelsWithCategories(device.models, fallbackCategories, {
      deviceId: device.id ?? currentDevice.id ?? null,
      channelName: device.name || currentDevice.name || computeChannelName(device),
      gatewayId: gatewayId.value
    })
    availableModelKeys.value = Object.keys(device.models || {})
  } finally {
    modelLoading.value = false
  }
}

const saveAllConfig = async () => {
  try {
    saving.value = true
    // 保存时将秒转毫秒
    const converted = devices.value.map(d => {
      const {
        upload_interval,
        deviceType,
        deviceSn,
        onlineState,
        subDeviceCount,
        address,
        port,
        slave_id,
        timeout,
        ...rest
      } = d
      const mergedModels = mergeModelsWithCategories(rest.models, Object.keys(rest.models || {}), {
        deviceId: rest.id,
        channelName: rest.name,
        gatewayId: gatewayId.value
      })
      return {
        ...rest,
        connection: normalizeConnectionConfig(rest.connection, {}, {
          protocol: rest.protocol,
          isRtu: isRtuProtocol(rest.protocol)
        }),
        models: normalizeModels(mergedModels),
        poll_interval: (typeof d.poll_interval === 'number' && d.poll_interval > 0)
          ? d.poll_interval * 1000 : d.poll_interval
      }
    })
    const config = { devices: converted }
    await configAPI.updateDeviceConfig(config)
    ElMessage.success('设备配置保存成功')
  } catch (error) {
    ElMessage.error('保存设备配置失败: ' + error.message)
  } finally {
    saving.value = false
  }
}

const openDeviceDialog = async (device = null) => {
  if (!protocolOptions.value.length && !protocolsLoading.value) {
    await loadProtocols()
  }
  if (device) {
    isEditing.value = true
    channelNameCustomized.value = true
    // 确保connection对象存在，兼容旧数据格式
    const legacyConnection = {
      address: device.address || '',
      port: device.port,
      slave_id: device.slave_id,
      timeout: device.timeout
    }
    const deviceData = {
      ...device,
      connection: normalizeConnectionConfig(device.connection, legacyConnection, {
        protocol: device.protocol,
        isRtu: isRtuProtocol(device.protocol)
      }),
      models: normalizeModels(device.models)
    }
    Object.assign(currentDevice, deviceData)
    const autoName = computeChannelName(currentDevice)
    if (!currentDevice.name) {
      channelNameCustomized.value = false
      currentDevice.name = autoName
    } else {
      channelNameCustomized.value = currentDevice.name !== autoName
    }
    await syncDeviceModelsWithProtocol(deviceData.protocol_config, currentDevice)
  } else {
    isEditing.value = false
    resetCurrentDevice()
  }
  serialPortPopoverVisible.value = false
  serialPortFetched.value = false
  serialPortOptions.value = []
  serialPortLoading.value = false
  deviceDialogVisible.value = true
}

const resetCurrentDevice = () => {
  Object.assign(currentDevice, {
    id: null,
    name: '',
    protocol: '',
    connection: createDefaultConnection(),
    // 默认5秒（保存时转换为毫秒）
    poll_interval: 5,
    models: {},
    enabled: true,
    description: '',
    status: 'offline',
    last_update: null,
    testing: false,
    protocol_config: '',
    type: ''
  })
  availableModelKeys.value = []
  modelLoading.value = false
  channelNameCustomized.value = false
  serialPortPopoverVisible.value = false
  serialPortOptions.value = []
  serialPortFetched.value = false
  serialPortLoading.value = false
  currentDevice.name = computeChannelName(currentDevice)
}

const handleProtocolChange = (protocol) => {
  currentDevice.connection = normalizeConnectionConfig({ ...currentDevice.connection }, {}, {
    protocol,
    isRtu: isRtuProtocol(protocol)
  })
  const defaults = deviceDefaults[protocol]
  if (defaults && defaults.port !== undefined && !isRtuProtocol(protocol)) {
    currentDevice.connection.port = defaults.port
  }
}

const isModbusProtocol = (protocol) => {
  return protocol === 'tcpmodbus' || protocol === 'modbus_tcp' || isRtuProtocol(protocol)
}

const saveDevice = async () => {
  if (!deviceFormRef.value) return

  let snapshot
  try {
    const valid = await deviceFormRef.value.validate()
    if (!valid) return

    saving.value = true

    snapshot = JSON.parse(JSON.stringify(devices.value))

    currentDevice.connection = normalizeConnectionConfig({ ...currentDevice.connection }, {}, {
      protocol: currentDevice.protocol,
      isRtu: isRtuProtocol(currentDevice.protocol)
    })

    if (isEditing.value) {
      // 更新设备
      const index = devices.value.findIndex(d => d.id === currentDevice.id)
      if (index !== -1) {
        devices.value[index] = JSON.parse(JSON.stringify({
          ...currentDevice,
          models: mergeModelsWithCategories(currentDevice.models, Object.keys(currentDevice.models || {}), {
            deviceId: currentDevice.id,
            channelName: currentDevice.name,
            gatewayId: gatewayId.value
          })
        }))
      }
    } else {
      // 添加设备
      const newDevice = {
        ...JSON.parse(JSON.stringify({
          ...currentDevice,
          models: mergeModelsWithCategories(currentDevice.models, Object.keys(currentDevice.models || {}), {
            deviceId: currentDevice.id,
            channelName: currentDevice.name,
            gatewayId: gatewayId.value
          })
        })),
        id: Date.now(),
        status: 'offline',
        last_update: null,
        testing: false
      }
      devices.value.push(newDevice)
    }

    // 保存到后端（秒->毫秒），同时剔除历史MQTT字段
    const converted = devices.value.map(d => {
      const {
        upload_interval,
        deviceType,
        deviceSn,
        onlineState,
        subDeviceCount,
        address,
        port,
        slave_id,
        timeout,
        ...rest
      } = d
      return {
        ...rest,
        connection: normalizeConnectionConfig(rest.connection, {}, {
          protocol: rest.protocol,
          isRtu: isRtuProtocol(rest.protocol)
        }),
        models: normalizeModels(mergeModelsWithCategories(rest.models, Object.keys(rest.models || {}), {
          deviceId: rest.id,
          channelName: rest.name,
          gatewayId: gatewayId.value
        })),
        poll_interval: (typeof d.poll_interval === 'number' && d.poll_interval > 0)
          ? d.poll_interval * 1000 : d.poll_interval
      }
    })
    const config = { devices: converted }
    await configAPI.updateDeviceConfig(config)

    ElMessage.success(isEditing.value ? '设备信息更新成功' : '设备添加成功')
    deviceDialogVisible.value = false
    nextTick(() => {
      deviceFormRef.value?.resetFields()
      resetCurrentDevice()
    })
  } catch (error) {
    // 回滚本地修改
    if (snapshot) {
      devices.value = snapshot
    }
    ElMessage.error('保存设备失败: ' + (error?.message || error))
  } finally {
    saving.value = false
  }
}

const editDevice = (device) => {
  openDeviceDialog(device)
}

const deleteDevice = async (device) => {
  try {
    await ElMessageBox.confirm(
      `确定要删除设备 "${device.name}" 吗？此操作不可恢复。`,
      '删除设备',
      {
        confirmButtonText: '确定删除',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )

    const index = devices.value.findIndex(d => d.id === device.id)
    if (index !== -1) {
      devices.value.splice(index, 1)

      // 保存到后端（统一转换/过滤）
    const converted = devices.value.map(d => {
      const { upload_interval, deviceType, deviceSn, onlineState, subDeviceCount, ...rest } = d
      return {
        ...rest,
        connection: normalizeConnectionConfig(rest.connection, {}, {
          protocol: rest.protocol,
          isRtu: isRtuProtocol(rest.protocol)
        }),
        models: normalizeModels(mergeModelsWithCategories(rest.models, Object.keys(rest.models || {}), {
          deviceId: rest.id,
          channelName: rest.name,
          gatewayId: gatewayId.value
        })),
        poll_interval: (typeof d.poll_interval === 'number' && d.poll_interval > 0)
          ? d.poll_interval * 1000 : d.poll_interval
      }
    })
      const config = { devices: converted }
      await configAPI.updateDeviceConfig(config)

      ElMessage.success('设备删除成功')
    }
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('删除设备失败: ' + error.message)
    }
  }
}

const testDevice = async (device) => {
  try {
    device.testing = true
    const result = await deviceAPI.testConnection(device.id)
    
    if (result.success) {
      device.status = 'online'
      device.last_update = Date.now()
      ElMessage.success(`设备 "${device.name}" 连接测试成功`)
    } else {
      device.status = 'error'
      ElMessage.error(`设备 "${device.name}" 连接测试失败: ${result.message}`)
    }
  } catch (error) {
    device.status = 'error'
    ElMessage.error(`设备连接测试失败: ${error.message}`)
  } finally {
    device.testing = false
  }
}

const handleSearch = () => {
  // 搜索逻辑在计算属性中实现
}

const handleFilter = () => {
  // 过滤逻辑在计算属性中实现
}

const handleSelectionChange = (selection) => {
  selectedDevices.value = selection
}

watch(
  () => currentDevice.protocol_config,
  async (value, oldValue) => {
    if (!deviceDialogVisible.value) return
    if (value && value === oldValue) return
    await loadGatewayIdentity()
    await syncDeviceModelsWithProtocol(value, currentDevice)
  }
)

watch(gatewayId, () => {
  if (!deviceDialogVisible.value) return
  const autoName = computeChannelName(currentDevice)
  const channelForModels = currentDevice.name || autoName
  if (currentDevice.protocol_config) {
    syncDeviceModelsWithProtocol(currentDevice.protocol_config, currentDevice)
  } else {
    currentDevice.models = mergeModelsWithCategories(currentDevice.models, Object.keys(currentDevice.models || {}), {
      deviceId: currentDevice.id ?? null,
      channelName: channelForModels,
      gatewayId: gatewayId.value
    })
  }
})

watch(() => currentDevice.name, (value) => {
  if (!deviceDialogVisible.value) return
  const autoName = computeChannelName(currentDevice)
  if (value === autoName) {
    channelNameCustomized.value = false
  }
  const categories = availableModelKeys.value.length > 0
    ? availableModelKeys.value
    : Object.keys(currentDevice.models || {})
  currentDevice.models = mergeModelsWithCategories(currentDevice.models, categories, {
    deviceId: currentDevice.id ?? null,
    channelName: value || autoName,
    gatewayId: gatewayId.value
  })
})

watch(
  () => [
    currentDevice.protocol,
    currentDevice.connection?.port,
    currentDevice.connection?.slave_id,
    currentDevice.connection?.address
  ],
  () => {
    if (!deviceDialogVisible.value) return
    const autoName = computeChannelName(currentDevice)
    if (currentDevice.name === autoName) {
      channelNameCustomized.value = false
    }
    if (!channelNameCustomized.value && autoName && currentDevice.name !== autoName) {
      channelNameCustomized.value = false
      currentDevice.name = autoName
    }
    const categories = availableModelKeys.value.length > 0
      ? availableModelKeys.value
      : Object.keys(currentDevice.models || {})
    currentDevice.models = mergeModelsWithCategories(currentDevice.models, categories, {
      deviceId: currentDevice.id ?? null,
      channelName: currentDevice.name || autoName,
      gatewayId: gatewayId.value
    })
  },
  { deep: true }
)

watch(() => currentDevice.protocol, (value) => {
  if (!isRtuProtocol(value)) {
    serialPortPopoverVisible.value = false
    serialPortOptions.value = []
    serialPortFetched.value = false
  }
})

watch(deviceDialogVisible, (visible) => {
  if (!visible) {
    serialPortPopoverVisible.value = false
  }
})

const batchTest = async () => {
  for (const device of selectedDevices.value) {
    await testDevice(device)
  }
}

const batchDelete = async () => {
  try {
    await ElMessageBox.confirm(
      `确定要删除选中的 ${selectedDevices.value.length} 个设备吗？此操作不可恢复。`,
      '批量删除设备',
      {
        confirmButtonText: '确定删除',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )

    selectedDevices.value.forEach(device => {
      const index = devices.value.findIndex(d => d.id === device.id)
      if (index !== -1) {
        devices.value.splice(index, 1)
      }
    })

    // 保存到后端（统一转换/过滤）
    const converted = devices.value.map(d => {
      const { upload_interval, deviceType, deviceSn, onlineState, subDeviceCount, ...rest } = d
      return {
        ...rest,
        models: normalizeModels(mergeModelsWithCategories(rest.models, Object.keys(rest.models || {}), {
          deviceId: rest.id,
          channelName: rest.name,
          gatewayId: gatewayId.value
        })),
        poll_interval: (typeof d.poll_interval === 'number' && d.poll_interval > 0)
          ? d.poll_interval * 1000 : d.poll_interval
      }
    })
    const config = { devices: converted }
    await configAPI.updateDeviceConfig(config)

    ElMessage.success(`成功删除 ${selectedDevices.value.length} 个设备`)
    selectedDevices.value = []
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('批量删除设备失败: ' + error.message)
    }
  }
}

const handleDialogClose = () => {
  deviceFormRef.value?.resetFields()
  deviceDialogVisible.value = false
  channelNameCustomized.value = false
}

// 工具函数
const formatTime = (timestamp) => {
  if (!timestamp) return '--'
  return new Date(timestamp).toLocaleString()
}

const getStatusColor = (status) => {
  const colors = {
    online: '#52c41a',
    offline: '#8c8c8c',
    error: '#ff4d4f'
  }
  return colors[status] || '#8c8c8c'
}

const getStatusClass = (status) => {
  const classes = {
    online: 'status-indicator status-online',
    offline: 'status-indicator status-offline',
    error: 'status-indicator status-error'
  }
  return classes[status] || 'status-indicator status-offline'
}

const getStatusText = (status) => {
  const texts = {
    online: '在线',
    offline: '离线',
    error: '故障'
  }
  return texts[status] || '未知'
}

const getProtocolName = (protocol) => {
  const names = {
    tcpmodbus: 'Modbus TCP',
    rtumodbus: 'Modbus RTU',
    modbus_tcp: 'Modbus TCP', // 兼容旧
    modbus_rtu: 'Modbus RTU', // 兼容旧
    http: 'HTTP',
    mqtt: 'MQTT'
  }
  return names[protocol] || protocol
}

const getProtocolTagType = (protocol) => {
  const types = {
    tcpmodbus: 'primary',
    rtumodbus: 'success',
    modbus_tcp: 'primary', // 兼容旧
    modbus_rtu: 'success', // 兼容旧
    http: 'warning',
    mqtt: 'info'
  }
  return types[protocol] || 'info'
}

const describeModelState = (model) => {
  if (!model) return '默认'
  if (model.enabled === false) return '禁用'
  if (typeof model.upload_interval === 'number' && model.upload_interval > 0) {
    return `${model.upload_interval} 秒`
  }
  return '默认'
}

const getModelTooltip = (device) => {
  const models = device.models && typeof device.models === 'object' ? device.models : {}
  const keys = Object.keys(models)
  if (keys.length === 0) return '未配置物模型'
  return keys.map(key => {
    const info = models[key] || {}
    const meta = []
    if (info.deviceType) meta.push(`类型:${info.deviceType}`)
    if (info.deviceSn) meta.push(`SN:${info.deviceSn}`)
    const metaText = meta.length ? ` [${meta.join(' ')}]` : ''
    return `${getModelLabel(key)}: ${describeModelState(info)}${metaText}`
  }).join(' / ')
}

const getModelDisplayList = (device) => {
  const models = device.models && typeof device.models === 'object' ? device.models : {}
  const keys = Object.keys(models)
  if (keys.length === 0) {
    return [{ key: 'empty', label: '未配置', details: '--' }]
  }
  return keys.map(key => {
    const info = models[key] || {}
    const label = getModelLabel(key)
    const state = describeModelState(info)
    const sn = info.deviceSn ? info.deviceSn : ''
    const details = [state, sn].filter(Boolean).join(' / ')
    return {
      key,
      label,
      details: details || '默认'
    }
  })
}

// 生命周期
onMounted(async () => {
  await loadGatewayIdentity()
  await loadProtocols()
  await loadDevices()
})
</script>

<style scoped>
.device-config {
  max-width: 1200px;
  margin: 0 auto;
}

.config-card {
  margin-bottom: 20px;
}


.card-header {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
}

.header-actions {
  margin-left: auto;
  display: flex;
  gap: 12px;
}

.filter-section {
  margin-bottom: 20px;
  padding: 16px;
  background: #fafafa;
  border-radius: 6px;
}

.device-name {
  display: flex;
  align-items: center;
  gap: 8px;
}

.batch-actions {
  margin-top: 16px;
}

.batch-buttons {
  display: flex;
  gap: 8px;
  margin-left: 12px;
}

.form-tip {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
  font-size: 12px;
  color: #909399;
}

.serial-port-search {
  cursor: pointer;
  color: var(--el-color-primary);
}

.serial-port-search:hover {
  color: var(--el-color-primary-dark-2, #337ecc);
}

.serial-port-popover {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.serial-port-popover__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 13px;
  font-weight: 500;
}

.serial-port-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 4px 0;
}

.serial-port-list :deep(.el-radio) {
  margin-right: 0;
  display: flex;
  align-items: center;
  width: 100%;
}

.serial-port-list :deep(.el-radio__label) {
  flex: 1;
  text-align: left;
}

.form-tip .el-icon {
  font-size: 12px;
}

.dialog-footer {
  text-align: right;
}

.model-intervals {
  display: flex;
  gap: 6px;
}

/* 物模型列表多行展示，提升可读性 */
.model-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 12px;
  color: #606266;
}

.model-list-item {
  display: flex;
  flex-direction: column;
  border-bottom: 1px dashed #e4e7ed;
  padding-bottom: 4px;
}

.model-list-item:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.model-list-name {
  font-weight: 600;
  color: #303133;
}

.model-list-details {
  color: #909399;
}

.model-blocks {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.model-block {
  border: 1px solid #ebeef5;
  border-radius: 6px;
  padding: 12px;
  background-color: #fafafa;
}

.model-block-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}

.model-block-title {
  font-weight: 600;
  color: #303133;
}

.model-block-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.model-form-item :deep(.el-form-item__label) {
  font-weight: 500;
}

.model-loading {
  margin: 12px 0;
}

.model-empty-tip {
  margin-bottom: 12px;
}

.model-tip {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: #909399;
  margin-top: -4px;
  margin-bottom: 8px;
}

.model-tip .el-icon {
  font-size: 12px;
}

.number-input {
  width: 200px;
}

:deep(.model-readonly .el-input__wrapper) {
  background-color: #f5f7fa;
  cursor: default;
}

.model-readonly {
  width: 100%;
}

:deep(.number-input .el-input__wrapper) {
  height: var(--el-component-size, 36px);
}

@media (max-width: 1024px) {
  .number-input {
    width: 100%;
  }
}

/* 调整设备编辑弹窗内容区域为可滚动，避免内容被遮挡 */
.device-edit-dialog :deep(.el-dialog__body) {
  max-height: 70vh;
  overflow-y: auto;
}

/* 表单标签尽量不换行，提升可读性 */
.device-edit-dialog :deep(.el-form-item__label) {
  white-space: nowrap;
}

/* 小屏下弹窗自适应宽度 */
@media (max-width: 768px) {
  .device-edit-dialog :deep(.el-dialog) {
    width: 95vw !important;
    margin: 0 auto;
  }
}

:deep(.el-table) {
  margin-bottom: 16px;
}

:deep(.el-form-item) {
  margin-bottom: 20px;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .header-actions {
    flex-direction: column;
    gap: 8px;
  }
  
  .header-actions .el-button {
    width: 100%;
  }
  
  .filter-section .el-col {
    margin-bottom: 12px;
  }
  
  :deep(.el-table .el-table__cell) {
    padding: 8px 4px;
  }
  
  .batch-buttons {
    flex-direction: column;
    margin-left: 0;
    margin-top: 8px;
  }
}
</style>
